/**
 * Copyright (c) 2011 Roundarch (www.roundarch.com)
 *
 * Author Joshua Jung
 *
 * ---
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.roundarch.flex.controls.graphControlClasses
{
    import com.roundarch.flex.controls.GraphControlBase;
    import com.roundarch.flex.controls.graphControlClasses.events.GraphControlSelectorEvent;
    import com.roundarch.flex.controls.graphControlConstraints.ISelectorConstraint;
    import com.roundarch.flex.controls.graphControlConstraints.SelectorConstraintUtil;
    import com.roundarch.flex.controls.graphControlInterfaces.IGraphControlSelectorPositioned;
    import com.roundarch.flex.controls.graphControlInterfaces.IIntervalCollection;
    import com.roundarch.flex.util.MouseRecord;

    import flash.display.DisplayObject;
    import flash.events.Event;
    import flash.events.IEventDispatcher;
    import flash.events.MouseEvent;
    import flash.geom.Point;
    import flash.utils.setTimeout;

    import mx.core.IInvalidating;
    import mx.core.IUITextField;
    import mx.core.UIComponent;
    import mx.core.UITextField;

    //-----------------------------
    //  Styles
    //-----------------------------
    /**
     *  The default skin for this selector.
     *
     *  @default "null"
     */
    [Style(name = "skinDefault", type = "Class", inherit = "no")]
    /**
     *  The default skin for this selector.
     *
     *  @default "null"
     */
    [Style(name = "skinOver", type = "Class", inherit = "no")]
    /**
     *  The default skin for this selector.
     *
     *  @default "null"
     */
    [Style(name = "skinDrag", type = "Class", inherit = "no")]

    /**
     * The width of the skin (if the skin needs it)
     *
     *  @default 12
     */
    [Style(name = "skinWidth", type = "Number", format = "Length", inherit = "no")]

    /**
     * The height of the skin (if the skin needs it)
     *
     *  @default 12
     */
    [Style(name = "skinHeight", type = "Number", format = "Length", inherit = "no")]

    /**
     * A graph control selector represents (that is a loaded word) a selected value in
     * a GraphControl.
     *
     * By represents, the selector simply updates its visual state based upon a value, but
     * should not directly control the selected values of a GraphControl.
     *
     * A GraphControlSelector should work to allow user interaction with a GraphControl. The idea
     * is that the user does something with a selector (like drag and drop it) and the selector
     * dispatches a GraphControlSelectorEvent requesting the selected value it represents to be
     * updated. The GraphControl determines determines which this is allowed or not and if it is
     * the GraphControl updates the selected value and the <code>value</code> property of the
     * selector.
     *
     * A GraphControlSelector should *only* update its position / display for the value it
     * represents when set value is called from the parent GraphControl.
     *
     * In this way, the parent GraphControl can manage numerous child selectors and selected values
     * without the selectors going rogue.
     *
     * @author jjung
     */
    /**
     *
     * @author jjung
     */
    public class GraphControlSelector extends UIComponent implements IGraphControlSelectorPositioned
    {
        //---------------------------------------------------------------------
        //
        //  Variables
        //
        //---------------------------------------------------------------------
        /**
         * The scroll speed in values per second, not pixels per second.
         */
        public var hScrollSpeed : Number = 1.0;

        /**
         * The scroll speed in values per second, not pixels per second.
         */
        public var vScrollSpeed : Number = 1.0;

        protected var explicitRequestedValue : Point;

        /**
         * Non-null when the mouse is down. Contains the MouseEvent for
         * when MOUSE_DOWN was dispatched.
         *
         * @default null
         */
        protected var mouseDownRecord : MouseRecord = null;

        /**
         * True when the mouse is dragging.
         */
        protected var mouseDragging : Boolean = false;
        protected var mouseLastRecord : MouseRecord = null;

        /**
         * True when the mouse is rolled over.
         */
        protected var mouseRollOver : Boolean = false;

        /**
         * The current skin of this graph control selector.
         */
        protected var skin : DisplayObject;

        /**
         * The value reprsented before drag started.
         */
        protected var valuePreviousToDrag : Point;

        private var autoScrollKey : uint;
        private var lastP : Point;

        /**
        * Okay when the user begins moving the selector, we want to make sure that we move it
        * relative to where the mouse was clicked on it - especially if it is a really large selector.
        */
        protected var mouseDownRelToSkinCenter : Point;

        //---------------------------------------------------------------------
        //
        //  Properties
        //
        //---------------------------------------------------------------------
        //-----------------------------
        // alwaysVisible
        //-----------------------------
        /**
         * @private
         */
        protected var _alwaysVisible : Boolean = false;

        [Bindable("alwaysVisibleChanged")]
        /**
         *
         *
         * @default false
         */
        public function get alwaysVisible() : Boolean
        {
            return _alwaysVisible;
        }

        /**
         * @private
         */
        public function set alwaysVisible(value : Boolean) : void
        {
            if (value == _alwaysVisible)
            {
                return;
            }

            _alwaysVisible = value;

            dispatchEvent(new Event("alwaysVisibleChanged"));
        }

        //-----------------------------
        // bringToFrontOnClick
        //-----------------------------
        /**
         * @private
         */
        protected var _bringToFrontOnClick : Boolean = true;

        [Bindable("bringToFrontOnClickChanged")]
        /**
         * When true this selector is brought to the front (child index of parent) when it is selected by the mouse.
         *
         * @default true
         */
        public function get bringToFrontOnClick() : Boolean
        {
            return _bringToFrontOnClick;
        }

        /**
         * @private
         */
        public function set bringToFrontOnClick(value : Boolean) : void
        {
            if (value == _bringToFrontOnClick)
            {
                return;
            }

            _bringToFrontOnClick = value;

            dispatchEvent(new Event("bringToFrontOnClickChanged"));
        }

        //-----------------------------
        // offsetY
        //-----------------------------
        /**
         * @private
         */
        protected var _offsetY : Number = 0;

        [Bindable("offsetYChanged")]
        /**
         * The Y offset by which to shift the position of this selector.
         *
         * @default 0
         */
        public function get offsetY() : Number
        {
            return _offsetY;
        }

        /**
         * @private
         */
        public function set offsetY(value : Number) : void
        {
            if (value == _offsetY)
            {
                return;
            }

            _offsetY = value;

            dispatchEvent(new Event("offsetYChanged"));
        }

        //-----------------------------
        // autoPosition
        //-----------------------------
        /**
         * @private
         */
        protected var _autoPosition : Boolean = true;

        /**
         * Set to false to turn off auto-positioning by the parent GraphControl.
         *
         * @default true
         */
        public function get autoPosition() : Boolean
        {
            return _autoPosition;
        }

        //-----------------------------
        // constraints
        //-----------------------------
        /**
         * @private
         */
        protected var _constraints : Array = null;

        [Bindable("constraintsChanged")]
        /**
         *
         *
         * @default null
         */
        public function get constraints() : Array
        {
            return _constraints;
        }

        /**
         * @private
         */
        public function set constraints(value : Array) : void
        {
            if (value == _constraints)
            {
                return;
            }

            _constraints = value;

            if (_constraints)
            {
                for each (var constraint : ISelectorConstraint in _constraints)
                {
                    if (constraint is IEventDispatcher)
                    {
                        (constraint as IEventDispatcher).addEventListener("change", constraint_changeHandler, false, 0, true);
                    }
                }
            }
            dispatchEvent(new Event("constraintsChanged"));
        }

        //-----------------------------
        // hInterval
        //-----------------------------
        /**
         * @private
         */
        protected var _hInterval : IIntervalCollection = null;

        [Bindable("hIntervalChanged")]
        /**
         * The horizontal interval this selector is bound to.
         *
         * @default null
         */
        public function get hInterval() : IIntervalCollection
        {
            return _hInterval;
        }

        /**
         * @private
         */
        public function set hInterval(value : IIntervalCollection) : void
        {
            if (value == _hInterval)
            {
                return;
            }

            _hInterval = value;

            if (_hInterval)
            {
                _hInterval.addEventListener("change", dependencyChangeHandler);
            }

            invalidateProperties();

            dispatchEvent(new Event("hIntervalChanged"));
        }

        //-----------------------------
        // parentGC
        //-----------------------------
        /**
         * @private
         */
        protected var _parentGC : GraphControlBase = null;

        [Bindable("parentGCChanged")]
        /**
         * The parent GraphControl of this selector.
         *
         * @default null
         */
        public function get parentGC() : GraphControlBase
        {
            return _parentGC;
        }

        /**
         * @private
         */
        public function set parentGC(value : GraphControlBase) : void
        {
            if (value == _parentGC)
            {
                return;
            }

            _parentGC = value;

            if (_parentGC)
            {
                _parentGC.addEventListener(MouseEvent.ROLL_OVER, parentGC_rollOverHandler);

            }
            invalidateProperties();

            dispatchEvent(new Event("parentGCChanged"));
        }

        //-----------------------------
        // scrollType
        //-----------------------------
        /**
         * @private
         */
        protected var _scrollType : String = GraphControlScrollType.OFF;

        [Bindable("scrollTypeChanged")]
        /**
         * "off", "move", or "resize"
         *
         * @default "off"
         */
        public function get scrollType() : String
        {
            return _scrollType;
        }

        /**
         * @private
         */
        public function set scrollType(value : String) : void
        {
            if (value == _scrollType)
            {
                return;
            }

            _scrollType = value;

            dispatchEvent(new Event("scrollTypeChanged"));
        }

        //-----------------------------
        // vInterval
        //-----------------------------
        /**
         * @private
         */
        protected var _vInterval : IIntervalCollection = null;

        //-----------------------------
        // placeBehindRenderers
        //-----------------------------
        /**
         * True if this selector should be placed behind the renderers.
         *
         * @default false
         */
        public function get placeBehindRenderers() : Boolean
        {
            return false;
        }

        //---------------------------------------------------------------------
        //
        //  Constructor
        //
        //---------------------------------------------------------------------
        /**
         *  Constructor
         */
        public function GraphControlSelector()
        {
            super();

            addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
            addEventListener(MouseEvent.CLICK, clickHandler);

            addEventListener(MouseEvent.ROLL_OVER, rollOverHandler);
            addEventListener(MouseEvent.ROLL_OUT, rollOutHandler);
        }

        //---------------------------------------------------------------------
        //
        //  Methods
        //
        //---------------------------------------------------------------------

        /**
         * In case are constriants are invalidated (if they change) we want to reapply
         * them all and request the new value from our parent GraphControl.
         */
        public function invalidateConstraints() : void
        {
            invalidateValue();
        }

        /**
         * Requests to update the value of this control. First applies all the available constraints.
         */
        public function invalidateValue(requested : Point = null) : void
        {
            if (!requested)
            {
                requested = value;
            }

            if (constraints)
            {
                requested = SelectorConstraintUtil.applyConstraints(requested, this, constraints);
            }

            parentGC.requestNewSelectorValue(this, requested);
        }

        //---------------------------------------------------------------------
        //
        //  Methods
        //
        //---------------------------------------------------------------------

        public function selectedPointChanged() : void
        {
            dispatchEvent(new Event("valueChanged"));
        }

        [Bindable("vIntervalChanged")]
        /**
         * The vertical interval this selector is bound to.
         *
         * @default null
         */
        public function get vInterval() : IIntervalCollection
        {
            return _vInterval;
        }

        /**
         * @private
         */
        public function set vInterval(value : IIntervalCollection) : void
        {
            if (value == _vInterval)
            {
                return;
            }

            _vInterval = value;

            if (_vInterval)
            {
                _vInterval.addEventListener("change", dependencyChangeHandler);
            }

            invalidateProperties();

            dispatchEvent(new Event("vIntervalChanged"));
        }

        //-----------------------------
        // value
        //-----------------------------
        [Bindable("valueChanged")]
        /**
         * The value this graph control selector currently represents.
         *
         * @default NaN
         */
        public function get value() : Point
        {
            if (!parentGC)
            {
                return null;
            }

            return parentGC.getSelectedPointFor(this);
        }

        /**
         * Setting a value on a selector has to go through the process
         * of applying all constraints and then requesteding the new
         * value from the GraphControl. The value you set may be change
         * by any constraints.
         */
        public function set value(value : Point) : void
        {
            explicitRequestedValue = value;

            invalidateProperties();
        }

        protected function autoScrollCallback() : void
        {
            // As long as we still have a key, we continue the autoscroll
            if (autoScrollKey)
            {
                trace("attempting update" + new Date().time);
                var sp : Point = parentGC.localToGlobal(new Point(0, 0));

                if (mouseLastRecord.stageX < sp.x)
                {
                    lastP.x -= hScrollSpeed;
                }
                if (mouseLastRecord.stageX > sp.x + parentGC.width)
                {
                    lastP.x += hScrollSpeed;
                }
                if (mouseLastRecord.stageY < sp.y)
                {
                    lastP.y -= vScrollSpeed;
                }
                if (mouseLastRecord.stageY > sp.y + parentGC.height)
                {
                    lastP.y += vScrollSpeed;
                }

                invalidateValue(lastP);

                lastP = new Point(value.x, value.y);

                dispatchEvent(new GraphControlSelectorEvent(GraphControlSelectorEvent.USER_CHANGE, value));

                autoScrollKey = setTimeout(autoScrollCallback, 50);
            }
        }

        /**
         * @inheritDoc
         */
        override protected function commitProperties() : void
        {
            super.commitProperties();

            // We don't want to reset our explicitRequestedValue until
            // we know for sure that the parentGC exists and can handle
            // our request.
            if (parentGC && explicitRequestedValue)
            {
                invalidateValue(explicitRequestedValue);

                // In the case of a newly requested explicit value, we want to
                // ask for it from our parent graph control
                explicitRequestedValue = null;
            }
            else if (value)
            {
                // If our intervals have changed we may need to align to the new ones, in
                // which case we refresh our value.
                invalidateValue(value);
            }

            if (!skin)
            {
                skinUpdate();
            }
        }

        protected function constraint_changeHandler(event : Event) : void
        {
            invalidateConstraints();
        }

        protected function dependencyChangeHandler(event : Event) : void
        {
            parentGC.requestRefreshSelectorValue(this);
        }

        //---------------------------------------------------------------------
        //
        //  Overridden Methods
        //
        //---------------------------------------------------------------------
        /**
         * @inheritDoc
         */
        override protected function measure() : void
        {
            super.measure();

            measuredMinWidth = measuredWidth = 20;
            measuredMinHeight = measuredHeight = 20;
        }

        /**
         * Updates the skin based on the current state.
         */
        protected function skinUpdate() : void
        {
            var skinDrag : Class = getStyle("skinDrag") as Class;
            var skinOver : Class = getStyle("skinOver") as Class;
            var skinDefault : Class = getStyle("skinDefault") as Class;

            if (mouseDragging && skinDrag)
            {
                if (skin && !(skin is skinDrag))
                {
                    removeChild(skin);

                    skin = new skinDrag(this);

                    addChild(skin);
                }
            }
            else if (mouseRollOver && skinOver)
            {
                if (skin && !(skin is skinOver))
                {
                    removeChild(skin);

                    skin = new skinOver(this);

                    addChild(skin);
                }
            }
            else
            {
                if (!skin || !(skin is skinDefault))
                {
                    if (skin)
                    {
                        removeChild(skin);
                    }

                    skin = new skinDefault(this);

                    addChild(skin);
                }
            }

            if (skin is IInvalidating)
            {
                (skin as IInvalidating).invalidateDisplayList();
            }
        }

        //---------------------------------------------------------------------
        //
        //  Events
        //
        //---------------------------------------------------------------------
        /**
         * @private
         */
        protected function clickHandler(event : MouseEvent) : void
        {
            event.stopImmediatePropagation();
        }

        /**
         * @private
         */
        protected function mouseDownHandler(event : MouseEvent) : void
        {
            stage.addEventListener(MouseEvent.MOUSE_MOVE, stage_mouseMoveHandler);
            stage.addEventListener(MouseEvent.MOUSE_UP, stage_mouseUpHandler);

            // This is so f'ing lame that MouseEvent.clone doesn't clone
            // the pixel position of MouseEvent stuff.
            mouseDownRecord = new MouseRecord(event);

            // We need to figure out where the mouse was down relative to our center.
            var center : Point = localToGlobal(new Point(0, 0));
            mouseDownRelToSkinCenter = new Point(event.stageX - center.x, event.stageY - center.y);

            valuePreviousToDrag = new Point(value.x, value.y);

            if (bringToFrontOnClick)
            {
                parentGC.setSelectorFocus(this);
            }

            skinUpdate();

            event.stopImmediatePropagation();
        }

        /**
         * Generic mouse up event "handler".
         */
        protected function mouseUp(event : MouseEvent) : void
        {
            if (!autoScrollKey)
            {
                requestNewValueFromUserDrag(event);
            }
            else
            {
                autoScrollKey = 0;
            }

            stage.removeEventListener(MouseEvent.MOUSE_MOVE, stage_mouseMoveHandler);
            stage.removeEventListener(MouseEvent.MOUSE_UP, stage_mouseUpHandler);

            mouseDownRecord = null;
            mouseLastRecord = null;

            mouseDragging = false;

            skinUpdate();
        }

        /**
        * @private
        */
        protected function parentGC_rollOverHandler(event : MouseEvent) : void
        {
        }

        /**
         * @protected
         */
        protected function requestNewValueFromUserDrag(event : MouseEvent) : void
        {
            if (mouseDownRecord)
            {
                mouseLastRecord = new MouseRecord(event);

                var pRelToParent : Point = parentGC.globalToLocal(new Point(event.stageX, event.stageY));
                var destination : Point = new Point(pRelToParent.x - mouseDownRelToSkinCenter.x, pRelToParent.y - mouseDownRelToSkinCenter.y);

                var p : Point = GraphControlUtil.getValueforScreen(destination, parentGC);

                if (!parentGC.hitTestPoint(event.stageX, event.stageY))
                {
                    if (scrollType != GraphControlScrollType.OFF && !autoScrollKey)
                    {
                        lastP = p;

                        autoScrollKey = setTimeout(autoScrollCallback, 50);
                    }
                }
                else
                {
                    autoScrollKey = 0;

                    invalidateValue(p);

                    dispatchEvent(new GraphControlSelectorEvent(GraphControlSelectorEvent.USER_CHANGE, value));
                }
            }
        }

        /**
         * @private
         */
        protected function rollOutHandler(event : MouseEvent) : void
        {
            mouseRollOver = false;

            skinUpdate();

            event.stopImmediatePropagation();
        }

        /**
         * @private
         */
        protected function rollOverHandler(event : MouseEvent) : void
        {
            mouseRollOver = true;

            skinUpdate();

            event.stopImmediatePropagation();
        }

        /**
         * @private
         */
        protected function stage_mouseMoveHandler(event : MouseEvent) : void
        {
            if (mouseDownRecord)
            {
                mouseDragging = true;

                requestNewValueFromUserDrag(event);
            }

            skinUpdate();

            event.stopImmediatePropagation();
        }

        /**
         * @private
         */
        protected function stage_mouseUpHandler(event : MouseEvent) : void
        {
            mouseUp(event);

            event.stopImmediatePropagation();
        }
    }
}
